﻿using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Xml;

namespace BibToWord07Xml
{
    class Program
    {
        static string inFile;
        static string outFile;

        static bool debugMode = false;

        static List<string> entries;

        static int prevIndex = 0;

        static XmlDocument doc;

        //in dictionary so we dont have to keep typing it out!
        static Dictionary<string, string> nsMap = new Dictionary<string, string>() { {"b", "http://schemas.openxmlformats.org/officeDocument/2006/bibliography"} };

        //mapping of bib types to Word07 types
        static Dictionary<string,string> typeMap = new Dictionary<string,string>() {{ "@ARTICLE","JournalArticle"},
            {"@BOOK","Book"},
            {"@INCOLLECTION","BookSection"},
            {"@INPROCEEDINGS","ConferenceProceedings"},
            {"@MASTERSTHESIS","Masters Thesis"},
            {"@PHDTHESIS","PhD Thesis"}};

        static Dictionary<string, string> fieldMap = new Dictionary<string, string>() {{ "author","Author"},
            {"booktitle","ConferenceName"},
            {"address","City"},
            {"edition","Edition"},
            {"editor","Author"},
            {"isbn","StandardNumber"},
            {"journal","JournalName"},
            {"month","Month"},
            {"number","Issue"},
            {"publisher","Publisher"},
            {"school","Institution"},
            {"volume","Volume"},
            {"year","Year"},
            {"comment","Comments"},
            {"pages","Pages"},
            {"title","Title"}};


        static void Main(string[] args)
        {
            string bib;

            if (args.Length < 2)
            {
                PrintUsage(); 
                return;
            }

            //assign inFile and outFile
            inFile = args[0];           //.bib
            outFile = args[1];          //.xml

            if (args.Length > 2)
            {
                if (args[2].Equals("-d")) { debugMode = true;}
            }

            entries = new List<string>();
            doc = new XmlDocument();

            if (!File.Exists(inFile))
            {
                Console.WriteLine(string.Format("{0} does not exist",inFile));
                return;
            }

            if (Path.GetExtension(inFile) != ".bib")
            {
                Console.WriteLine(string.Format("{0} doesn't appear to be a .bib file",inFile));
                return;
            }

            StreamReader rdr = new StreamReader(inFile);
            bib = rdr.ReadToEnd();
            rdr.Close();

            //now bib string contains whole bib document
            //so split it up into indvidual entries
            int index;

            while (prevIndex!=-1 && (index=bib.IndexOf('@',prevIndex))!= -1)
            {
                //search for next @
                int endIndex = bib.IndexOf('@', index + 1);

                if (endIndex != -1)
                {
                    entries.Add(bib.Substring(index, endIndex - index));
                }

                else entries.Add(bib.Substring(index, bib.Length - index));

                prevIndex = endIndex;
               
            }

            //create a new xml document, which is going to be our Word07 compliant bibliography
            XmlElement elem = doc.CreateElement("b:Sources");
           
            //set namespaces on root element
            elem.SetAttribute("xmlns", "http://schemas.openxmlformats.org/officeDocument/2006/bibliography");
            elem.SetAttribute("xmlns:b", nsMap["b"]);

            doc.AppendChild(elem);
        
            //for each entry
            foreach (string str in entries)
            {
                XmlNode source;
                XmlNode sourceType;
                XmlNode nodeTag;

                int braceStart = str.IndexOf('{');
                string type = str.Substring(0, braceStart);
                string tag;

                string wordType;

                try
                {
                    //resolve type to Word07 entity
                    wordType = typeMap[type];

                    int commaIndex = str.IndexOf(',');
                    tag = str.Substring(braceStart + 1, str.IndexOf(',') - 1 - braceStart);
                    source = doc.CreateElement("b", "Source", nsMap["b"]);


                    nodeTag = doc.CreateElement("b", "Tag", nsMap["b"]);
                    nodeTag.AppendChild(doc.CreateTextNode(tag));


                    sourceType = doc.CreateElement("b", "SourceType", nsMap["b"]);
                    sourceType.AppendChild(doc.CreateTextNode(wordType));
                    //sourceType.Value = "Book";

                    source.AppendChild(nodeTag);
                    source.AppendChild(sourceType);

                    elem.AppendChild(source);

                    ProcessKeyValues(doc, source, str.Substring(commaIndex + 1, str.LastIndexOf('}') - 1 - commaIndex));
                }

                catch (KeyNotFoundException ex)
                {
                    if (debugMode) { Console.WriteLine("Unrecognised type: " + type); };
                }
            }

            Console.WriteLine(string.Format("Done, written XML file to: {0}", outFile));
            doc.Save(outFile);
        }

        /// <summary>
        /// Gets the index of a character specified in value, whilst preserving those that are in the protected area, specified by the start and endProtection
        /// characters
        /// </summary>
        /// <param name="value"></param>
        /// <param name="startProtection"></param>
        /// <param name="endProtection"></param>
        /// <param name="search"></param>
        /// <returns></returns>
        private static int IndexOfProtectedVal(char value, char startProtection, char endProtection,string search)
        {
            return IndexOfProtectedVal(value, startProtection, endProtection, search, 0);
        }

        /// <summary>
        /// Gets the index of a character specified in value, whilst preserving those that are in the protected area, specified by the start and endProtection
        /// characters, and starting from a position specified in startIndex
        /// </summary>
        /// <param name="value"></param>
        /// <param name="startProtection"></param>
        /// <param name="endProtection"></param>
        /// <param name="search"></param>
        /// <param name="startIndex"></param>
        /// <returns></returns>
        private static int IndexOfProtectedVal(char value, char startProtection, char endProtection, string search,int startIndex)
        {
            int level = 0;
            for (int i = startIndex; i < search.Length; i++)
            {
                if (search[i] == startProtection) level++;
                else if (search[i] == endProtection) level--;

                if (level == 0 && search[i] == value)
                {
                    return i;
                }
            }

            return -1;
        }

        //given a fragement of the entry, beginning at the first key and ending at the last value, get the key value pairs and parse them

        /// <summary>
        /// given a fragement of the entry, beginning at the first key and ending at the last value, get the key value pairs and parse them
        /// into their relevant Word2007 entities, by acting on the current XmlNode specified in source, corresponding to the current entry
        /// </summary>
        /// <param name="doc"></param>
        /// <param name="source"></param>
        /// <param name="entryPart"></param>
        private static void ProcessKeyValues(XmlDocument doc, XmlNode source, string entryPart)
        {
            int curIndex=0;

            int commaIndex=0;
            //from first key to last val
            
            //find first instance comma outside "value" braces
            while((commaIndex=IndexOfProtectedVal(',','{','}',entryPart,curIndex))!=-1) {

                string keyVal = entryPart.Substring(curIndex,commaIndex-curIndex);

                int equalIndex = keyVal.IndexOf('=');

                //now search for, and break entry when we've found first '='
                string key = keyVal.Substring(0, equalIndex).Replace(System.Environment.NewLine, string.Empty).TrimStart(' ').Trim();
                string val = keyVal.Substring(equalIndex + 1, keyVal.Length - equalIndex-1).Trim().TrimStart('{').TrimEnd('}').Replace(System.Environment.NewLine," ").Replace("\t",string.Empty);
                //string[] keyVal = entryPart.Split('=');

                XmlNode xmlNode;

                //provides mapping on bib entities to Word07 entities
                try
                {
                    xmlNode = doc.CreateElement("b", fieldMap[key], nsMap["b"]);

                    //is it an author tag - this requires special processing
                    if (key.Equals("author"))
                    {
                        XmlNode authorNode = doc.CreateElement("b", "Author", nsMap["b"]);

                        XmlNode personList = doc.CreateElement("b", "NameList", nsMap["b"]);
                        
                        //oops, spaces between "and" or "&" needed otherwise will split names up with "and" in names!
                        foreach (string author in val.Split(new string[] {" and "," & "},StringSplitOptions.RemoveEmptyEntries))
                        {
                            string firstName = string.Empty;
                            string lastName = string.Empty;
                            string[] nameSplit;
                            //process it
                            //Gergely Acs
                            if (author.IndexOfAny(new char[] { ',', '.' }) == -1)
                            {
                                nameSplit = author.Split(' ');
                                firstName = nameSplit[0].Trim();
                                lastName = nameSplit[1].Trim();

                            }

                            //Aickelin, U.
                            else if (author.IndexOf(',')!=-1)
                            {
                                nameSplit = author.Split(',');
                                firstName = nameSplit[1].Trim().TrimEnd('.');
                                lastName = nameSplit[0].TrimEnd(',');
                            }

                                 //U. Aickelin
                            else if (author.IndexOf('.') != -1)
                            {
                                nameSplit = author.Split('.');
                                firstName = nameSplit[0].Trim().TrimEnd('.');
                                lastName = nameSplit[1].Trim();

                            }
                            XmlNode personNode = doc.CreateElement("b", "Person", nsMap["b"]);

                            XmlNode lastNameNode = doc.CreateElement("b", "Last", nsMap["b"]);
                            XmlNode firstNameNode = doc.CreateElement("b", "First", nsMap["b"]);

                            firstNameNode.AppendChild(doc.CreateTextNode(firstName));
                            lastNameNode.AppendChild(doc.CreateTextNode(lastName));

                            personNode.AppendChild(firstNameNode);
                            personNode.AppendChild(lastNameNode);

                            personList.AppendChild(personNode);
                            authorNode.AppendChild(personList);


                        }

                        xmlNode.AppendChild(authorNode);



                    }

                    else
                    {
                  
                        xmlNode.AppendChild(doc.CreateTextNode(val)); 
                    }

                    source.AppendChild(xmlNode);
                }

                catch (KeyNotFoundException ex)
                {
                    if (debugMode) { Console.WriteLine("Cannot handle bib tag: " + key); }
                }

                curIndex = commaIndex + 1;
            }
        }

        private static void PrintUsage()
        {
            Console.WriteLine("Usage:");
            Console.WriteLine("BibToWord07Xml infile.bib outfile.xml -d");


        }
    }
}
